﻿using System;
using System.Collections.Specialized;
using System.Data;
using System.Globalization;
using System.IO;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Text;

using BizElements.Core;
using BizElements.BusinessLayer;

namespace BizElements.Web
{
    /// <summary>Provides webform context logic. Uses sesion state and query string to retrieve and store data.</summary>
    public sealed class WebFormContextManager : IWebFormContextManager
    {
        #region Constants & Configuration.

        static readonly string PopupName = "Biz_Common_WebFormContext";
        static readonly int SmallHeight = 300;
        static readonly int DefaultHeight = 600;
        static readonly int LargeHeight = 800;
        static readonly int SmallWidth = 400;
        static readonly int DefaultWidth = 800;
        static readonly int LargeWidth = 1000;

        const string JsReloadOpener = "window.opener.location.reload();";

        #endregion

        #region BuildUrl.

        /// <summary>Builds URL based on data provided in the context.</summary>
        /// <param name="ctx">Webform context.</param>
        public string BuildUrl(WebFormContext ctx)
        {
            string url = ctx.Url;
            if (ctx.EntityId != null)
                url = WebUtil.SetUrlParam(url, CommonQueryKeys.EntityId, ctx.EntityId.ToString());

            foreach (string param in ctx.Parameters.AllKeys)
                url = WebUtil.SetUrlParam(url, param, ctx.Parameters[param]);

            if (string.IsNullOrEmpty(ctx.ReturnUrl))
                ctx.ReturnUrl = HttpContext.Current.Request.RawUrl;

            if (ctx.Mode == OpenFormMode.Redirect)
                url = WebUtil.SetUrlParam(url, CommonQueryKeys.OnCloseUrl, ctx.ReturnUrl);

            if (ctx.ReadOnly)
                url = WebUtil.SetUrlParam(url, "ReadOnly", "1");

            if (ctx.ChildrenHidden)
                url = WebUtil.SetUrlParam(url, "NoChildren", "1");

            if (ctx.ReloadOpener)
                url = WebUtil.SetUrlParam(url, CommonQueryKeys.ReloadOpener, "1");

            url = WebUtil.SetUrlParam(url, WebFormContext.ContextQueryKey, ctx.ContextId);
            return url;
        }

        #endregion

        #region OpenUrl.

        /// <summary>Opens URL using specified options.</summary>
        /// <param name="mode">Mode: popup or redirect.</param>
        /// <param name="url">URL.</param>
        /// <param name="size">One of predefined popup sizes.</param>
        public void OpenUrl(OpenFormMode mode, string url, WindowSize size)
        {
            if (mode == OpenFormMode.Redirect)
            {
                HttpContext.Current.Response.Redirect(url);
            }
            else if (mode == OpenFormMode.Popup)
            {
                string absUrl = (HttpContext.Current.Handler as Page).ResolveClientUrl(url);
                JScript.OpenWindow(absUrl, PopupName, GetWidth(size), GetHeight(size), true);
            }
            else
            {
                throw new ArgumentException("Unsupported mode: " + mode.ToString(), "mode");
            }
        }

        private static int GetWidth(WindowSize size)
        {
            switch (size)
            {
                case WindowSize.Small:
                    return SmallWidth;
                case WindowSize.Large:
                    return LargeWidth;
                default:
                    return DefaultWidth;
            }
        }

        private static int GetHeight(WindowSize size)
        {
            switch (size)
            {
                case WindowSize.Small:
                    return SmallHeight;
                case WindowSize.Large:
                    return LargeHeight;
                default:
                    return DefaultHeight;
            }
        }

        #endregion

        #region Push.

        /// <summary>Stores context to internal structures. Clients retrieve the context via EnsureFormContext methods.</summary>
        /// <param name="context">Context to push.</param>
        public void Push(WebFormContext context)
        {
            HttpContext.Current.Session[context.ContextId] = context;
        }

        #endregion

        #region ParseEntityProperties.

        /// <summary>Initializes entity properties with values stored in Parameters collection.</summary>
        /// <param name="entity">Entity whose properties are parsed.</param>
        /// <param name="allParams">Name-value collection that contains entity data. Name/key matches property names.</param>
        /// <param name="criteria">Selects the group of properties to parse if the value is provided in Parameters collection.</param>
        public void ParseEntityProperties(IEntity entity, NameValueCollection allParams, PropertySelection criteria)
        {
            MessageFormatter formatter = UserSession.Formatter;
            foreach (IDbColumn field in entity.Table.Columns)
            {
                if (IsSelected(field, criteria))
                    ParseFieldFromParams(entity, allParams, field, formatter);
            }
        }

        private static void ParseFieldFromParams(IEntity entity, NameValueCollection allParams, IDbColumn field, MessageFormatter formatter)
        {
            string textValue = allParams[field.PropertyName];
            if (!string.IsNullOrEmpty(textValue))
            {
                object parsedValue = NumberParser.ParseValue(field.DataType, textValue, formatter);
                entity.SetField(field, parsedValue);
            }
        }

        private static bool IsSelected(IDbColumn field, PropertySelection criteria)
        {
            bool allFieldSelected = (criteria == PropertySelection.AllProperties);
            if (allFieldSelected)
                return true;

            bool pkSelected = ((criteria & PropertySelection.PrimaryKey) > 0);
            if (pkSelected && field.IsPrimaryKeyPart)
                return true;

            bool fkSelected = ((criteria & PropertySelection.ForeignKey) > 0);
            if (fkSelected && field.IsForeignKeyPart)
                return true;

            return false;
        }

        #endregion

        #region EnsureFormContext, Pop, ParseFromUrl.

        /// <summary>Initializes context if not alread initialized.</summary>
        /// <param name="formContext">Variable or field to initialize if not already initialized, ie. if it point to a nul reference.</param>
        /// <param name="uiWebFormContext">Hidden form field that contains XML serialized context data.</param>
        public void EnsureFormContext(ref WebFormContext formContext, HiddenField uiWebFormContext)
        {
            if (formContext != null)
                return;

            Page currPage = (Page)HttpContext.Current.Handler;
            if (!currPage.IsPostBack)
            {
                formContext = Pop();
                string xml = ToXml(formContext);
                uiWebFormContext.Value = HttpUtility.HtmlEncode(xml);
            }
            else
            {
                string xml = HttpUtility.HtmlDecode(uiWebFormContext.Value);
                formContext = ReadXml(xml);
            }
        }

        /// <summary>Initializes context if not alread initialized.</summary>
        /// <param name="formContext">Variable or field to initialize if not already initialized, ie. if it point to a nul reference.</param>
        /// <param name="uiWebFormContext">Hidden form field that contains XML serialized context data.</param>
        public void EnsureFormContext(ref WebFormContext formContext, ITextControl uiWebFormContext)
        {
            if (formContext != null)
                return;

            Page currPage = (Page)HttpContext.Current.Handler;
            if (!currPage.IsPostBack)
            {
                formContext = Pop();
                string xml = ToXml(formContext);
                uiWebFormContext.Text = HttpUtility.HtmlEncode(xml);
            }
            else
            {
                string xml = HttpUtility.HtmlDecode(uiWebFormContext.Text);
                formContext = ReadXml(xml);
            }
        }

        /// <summary>Pops context specified in current query string from user stack or URL.</summary>
        private static WebFormContext Pop()
        {
            string contextId = HttpContext.Current.Request.QueryString[WebFormContext.ContextQueryKey];
            WebFormContext context = HttpContext.Current.Session[contextId] as WebFormContext;
            if (context != null)
                HttpContext.Current.Session.Remove(contextId);
            else
                context = ParseFromUrl();

            return context;
        }

        private static WebFormContext ParseFromUrl()
        {
            var req = HttpContext.Current.Request;
            WebFormContext ctx = new WebFormContext();
            if (!string.IsNullOrEmpty(req.QueryString[WebFormContext.ContextQueryKey]))
                ctx.ContextId = req.QueryString[WebFormContext.ContextQueryKey];

            ctx.Url = req.Url.AbsolutePath;
            ctx.Title = TryGetCurrentPageTitle();
            ctx.EntityId = TextUtil.TrimNullableString(req[CommonQueryKeys.EntityId]);
            ctx.ReturnUrl = CommonQueryValues.OnCloseUrl;
            ctx.Mode = OpenFormMode.Redirect;
            if (req.QueryString["ReadOnly"] == "1")
                ctx.ReadOnly = true;
            if (req.QueryString["NoChildren"] == "1")
                ctx.ChildrenHidden = true;
            if (req.QueryString[CommonQueryKeys.ReloadOpener] == "1")
                ctx.ReloadOpener = true;
            
            // Heuristics - if no close URL but there is on-close script then we assume it's a popup.
            bool hasNoUrlButHasOnCloseScript = string.IsNullOrEmpty(ctx.ReturnUrl) && !string.IsNullOrEmpty(CommonQueryValues.OnCloseScript);
            if (hasNoUrlButHasOnCloseScript)
            {
                ctx.Mode = OpenFormMode.Popup;
            }

            foreach (string key in req.QueryString.AllKeys)
            {
                if ((key != CommonQueryKeys.EntityId) && (key != CommonQueryKeys.OnCloseUrl) && (key != WebFormContext.ContextQueryKey) && (key != "ReadOnly") && (key != "NoChildren"))
                    ctx.Parameters[key] = req.QueryString[key];
            }

            return ctx;
        }

        private static string TryGetCurrentPageTitle()
        {
            string title;
            Page currPage = HttpContext.Current.Handler as Page;
            if (currPage != null)
            {
                ITextControl header = currPage.FindControl("uiHeader") as ITextControl;
                title = (header != null) ? header.Text : currPage.GetType().Name;
            }
            else
            {
                title = "";
            }

            return title;
        }

        #endregion

        #region Private ToXml and ReadXml.

        private static string ToXml(WebFormContext ctx)
        {
            DataSet ds = new DataSet("WebFormContext");
            DataTable headerTable = ds.Tables.Add("Head");
            AddColumns(headerTable, "ContextId", "Url", "Title", "Mode", "Size", "EntityId", "ReturnUrl", "ReturnTitle", "ReadOnly", "ChildrenHidden", "ReloadOpener");
            DataRow header = headerTable.NewRow();
            header["ContextId"] = ctx.ContextId;
            header["Url"] = ctx.Url;
            header["Title"] = ctx.Title;
            header["Mode"] = ctx.Mode.ToString();
            header["Size"] = ctx.Size.ToString();
            header["EntityId"] = ctx.EntityId;
            header["ReturnUrl"] = ctx.ReturnUrl;
            header["ReturnTitle"] = ctx.ReturnTitle;
            header["ReadOnly"] = ctx.ReadOnly.ToString();
            header["ChildrenHidden"] = ctx.ChildrenHidden.ToString();
            header["ReloadOpener"] = ctx.ReloadOpener.ToString();

            headerTable.Rows.Add(header);

            DataTable paramsTable = ds.Tables.Add("Parameters");
            AddColumns(paramsTable, "Key", "Value");
            foreach (string par in ctx.Parameters.AllKeys)
                paramsTable.Rows.Add(par, ctx.Parameters[par]);

            DataTable returnPropertyMapTable = ds.Tables.Add("ReturnPropertyMap");
            AddColumns(returnPropertyMapTable, "Key", "Value");
            foreach (string mappedProperty in ctx.ReturnPropertyMap.AllKeys)
                returnPropertyMapTable.Rows.Add(mappedProperty, ctx.ReturnPropertyMap[mappedProperty]);

            DataTable propertyToControlMapTable = ds.Tables.Add("PropertyToControlMap");
            AddColumns(propertyToControlMapTable, "Key", "Value");
            foreach (string propertyName in ctx.PropertyToControlMap.AllKeys)
                propertyToControlMapTable.Rows.Add(propertyName, ctx.PropertyToControlMap[propertyName]);

            return ds.GetXml();
        }

        private static void AddColumns(DataTable table, params string[] columnNames)
        {
            foreach (string col in columnNames)
                table.Columns.Add(col, typeof(string));
        }

        private static WebFormContext ReadXml(string xml)
        {
            DataSet ds = new DataSet("WebFormContext");
            using (StringReader sr = new StringReader(xml))
            {
                ds.ReadXml(sr);
            }

            WebFormContext ctx = new WebFormContext();
            DataRow header = ds.Tables["Head"].Rows[0];
            ctx.ContextId = GetRowField(header, "ContextId");
            ctx.Url = GetRowField(header, "Url");
            ctx.Title = GetRowField(header, "Title");
            ctx.Mode = NumberParser.ParseEnum<OpenFormMode>(GetRowField(header, "Mode")) ?? OpenFormMode.Redirect;
            ctx.Size = NumberParser.ParseEnum<WindowSize>(GetRowField(header, "Size")) ?? WindowSize.Default;
            ctx.EntityId = GetRowField(header, "EntityId");
            ctx.ReturnUrl = GetRowField(header, "ReturnUrl");
            ctx.ReturnTitle = GetRowField(header, "ReturnTitle");
            ctx.ReadOnly = NumberParser.ParseBool(GetRowField(header, "ReadOnly")) ?? false;
            ctx.ChildrenHidden = NumberParser.ParseBool(GetRowField(header, "ChildrenHidden")) ?? false;
            ctx.ReloadOpener = NumberParser.ParseBool(GetRowField(header, "ReloadOpener")) ?? false;

            if (ds.Tables.Contains("Parameters") && ds.Tables["Parameters"].Columns.Count > 1)
            {
                foreach (DataRow par in ds.Tables["Parameters"].Rows)
                    ctx.Parameters[(string)par[0]] = (string)par[1];
            }

            if (ds.Tables.Contains("ReturnPropertyMap") && ds.Tables["ReturnPropertyMap"].Columns.Count > 1)
            {
                foreach (DataRow map in ds.Tables["ReturnPropertyMap"].Rows)
                    ctx.ReturnPropertyMap[(string)map[0]] = (string)map[1];
            }

            if (ds.Tables.Contains("PropertyToControlMap") && ds.Tables["PropertyToControlMap"].Columns.Count > 1)
            {
                foreach (DataRow map in ds.Tables["PropertyToControlMap"].Rows)
                    ctx.PropertyToControlMap[(string)map[0]] = (string)map[1];
            }

            return ctx;
        }

        private static string GetRowField(DataRow row, string columnName)
        {
            int fieldIdx = row.Table.Columns.IndexOf(columnName);
            string val = (fieldIdx >= 0) ? (string)row[fieldIdx] : null;
            return val;
        }

        #endregion

        #region GetReturnUrl.

        /// <summary>Build return URL used when current form was openned using redirect method.</summary>
        /// <param name="ctx">Webform context.</param>
        /// <param name="message">Ignored. Not implemented.</param>
        /// <returns>URL.</returns>
        public string GetReturnUrl(WebFormContext ctx, string message)
        {
            string url = ctx.ReturnUrl ?? CommonQueryValues.OnCloseUrl;
            if (string.IsNullOrEmpty(url))
                url = WebFormContext.DefaultPageRelativeUrl;

            return url;
        }

        #endregion

        #region ClosePopup.

        /// <summary>Closes popup windows and executes the specified script.</summary>
        /// <param name="ctx">Webform context.</param>
        /// <param name="onBeforeCloseScript">Optional script to execute before the window is closed. Typically a script that updates window openner.</param>
        public void ClosePopup(WebFormContext ctx, string onBeforeCloseScript)
        {
            // Reload opener window only if no other action is defined, such as copy bizobject properties to opener form.            
            // User defined script has preference for compatibility reasons, then automatically generated script. Reload only if specified.
            StringBuilder jsClose = new StringBuilder();
            string userDefined = TextUtil.TrimNullableString(CommonQueryValues.OnCloseScript);
            if (!string.IsNullOrEmpty(userDefined))
            {
                userDefined = TextUtil.EnsureSuffix(userDefined, ";");
                jsClose.Append(userDefined);
            }
            else if (!string.IsNullOrEmpty(onBeforeCloseScript))
            {
                jsClose.Append(onBeforeCloseScript);
            }

            if (ctx.ReloadOpener)
                jsClose.Append(JsReloadOpener);

            jsClose.Append(JScript.RenderCloseWindow());
            JScript.RegisterStartupScript(jsClose.ToString(), true);
        }

        #endregion
    }
}
